/**
 *  @file XmlSaxParser.cpp
 */
#include "XmlSaxParser.h"
#include "XmlTokenizer.h"
#include "../../../common/CppMemPool.h"
#include "../../../io/BufferedReadStream.h"
#include "../../../collections/ArrayT.h"
#include "../../../collections/StackT.h"
#include "../../../collections/HashtableT.h"
#include "../../../text/CFString.h"
#include <stdio.h>

using namespace cppflib::io;
using namespace cppflib::collections;

namespace cppflib
{

namespace xml
{

namespace sax
{


XmlSaxParser::XmlSaxParser(void)
{
   this->pStream = NULL;
   this->pHandler = NULL;
}

PRIVATE const int STREAMBUFSIZE = 16;

XmlSaxParser::XmlSaxParser(Stream &stream, SAXHandler &handler)
{
   this->pStream = new BufferedReadStream(&stream, STREAMBUFSIZE);
   cppmempool::Retain(this->pStream);
   this->pHandler = &handler;
   cppmempool::Retain(this->pHandler);
   this->pHandler->ClearErrorWarning();
}

XmlSaxParser::~XmlSaxParser(void)
{
   if (this->pStream)
      cppmempool::Release(this->pStream);
   if (this->pHandler)
      cppmempool::Release(this->pHandler);
}

PRIVATE void __RewindOneStep(Stream *pStream)
{
   pStream->Seek(-1, Stream::S_CUR);
}

/**
 *  Notify handler
 */
PRIVATE void __NotifyErrorToHandler(SAXHandler &h, SAXHandler::xmlhandlererror_t err, pcwstr_t pszFormat, ...)
{
   CFString s;
   s.__InternalFormat(&pszFormat, 0);
   
   h.MarkErrorWarning(err);
   switch(err) {
      case SAXHandler::HERR_WARN: h.Warning(s); break;
      case SAXHandler::HERR_ERROR: h.Error(s); break;
      case SAXHandler::HERR_FATAL: h.FatalError(s); break;
   }
}

/**
 *  Skip data bewteen '[' and ']>'
 *
 *  @param [in, out] curTokenType -- update token type as well if succeed
 *
 *  @return true -- ']>' is found
 *  @return false -- ']>' not found
 */
PRIVATE bool __SkipDTDInlineDeclaration(XmlTokenizer &tokenizer, XmlTokenizer::tokentype_t &curTokenType)
{
   Stream *pStream = tokenizer.GetStream();
   int c;
   
   while(true) {
      if ((c = pStream->Read()) < 0) break;
      if (c == '\n') tokenizer.IncrementLineNumber();
      if (c != ']') continue;
      if ((c = pStream->Read()) < 0) break;
      if (c != '>') { 
         __RewindOneStep(pStream);  // in case there is a "]]>" pattern
         continue; 
      }

      curTokenType = XmlTokenizer::XML_TAGCLOSE_3; // update token type (end of DTD)
      return true;
   }

   return false;
}

/**
 *  Skip Document Type Definition
 */
PRIVATE bool __SkipDTD(XmlTokenizer &tokenizer, SAXHandler &handler)
{
   XmlTokenizer::tokentype_t preTokenType = XmlTokenizer::XML_DOCTYPE_START;
   CFString token;
   bool isSystemStart = false, isPublicStart = false;
   int attrValCount = 0;
   bool isOK = true;

   while(true) {
      XmlTokenizer::tokentype_t curTokenType = tokenizer.GetToken(token);

      switch (preTokenType) {
         case XmlTokenizer::XML_DOCTYPE_START:
            if (curTokenType != XmlTokenizer::XML_ATTRNAME) isOK = false;
            break;
         case XmlTokenizer::XML_ATTRNAME:
            if (curTokenType == XmlTokenizer::XML_ATTRNAME) {
               if (token == _S("SYSTEM")) { curTokenType = XmlTokenizer::XML_KEYWORD_SYSTEM; isSystemStart = true;}
               else if (token == _S("PUBLIC")) { curTokenType = XmlTokenizer::XML_KEYWORD_PUBLIC; isPublicStart = true; }
               else isOK = false;
            }
            else if (curTokenType == XmlTokenizer::XML_KEYWORD_OPENSQUAREBRACKET) {
               isOK = __SkipDTDInlineDeclaration(tokenizer, curTokenType);
            }
            else {
               if (curTokenType != XmlTokenizer::XML_TAGCLOSE_2) 
                  isOK = false;
            }
            break;
         case XmlTokenizer::XML_KEYWORD_SYSTEM:
            if (curTokenType == XmlTokenizer::XML_ATTRVALUE) ++attrValCount;
            else isOK = false;
            break;
         case XmlTokenizer::XML_KEYWORD_PUBLIC:
            if (curTokenType == XmlTokenizer::XML_ATTRVALUE) ++attrValCount;
            else isOK = false;
            break;
         case XmlTokenizer::XML_ATTRVALUE:
            if (isSystemStart) {
               if (curTokenType == XmlTokenizer::XML_KEYWORD_OPENSQUAREBRACKET) {
                  isOK = __SkipDTDInlineDeclaration(tokenizer, curTokenType);
               }
               else {
                  if (curTokenType != XmlTokenizer::XML_TAGCLOSE_2) 
                     isOK = false;
               }
            }
            else if (isPublicStart) {
               if (curTokenType == XmlTokenizer::XML_KEYWORD_OPENSQUAREBRACKET) {
                  isOK = (attrValCount == 2) ? __SkipDTDInlineDeclaration(tokenizer, curTokenType) : false;
               }
               else if (curTokenType == XmlTokenizer::XML_ATTRVALUE) {
                  if (attrValCount == 1) attrValCount = 2; // max is 2
                  else isOK = false;
               }
               else {
                  if (curTokenType != XmlTokenizer::XML_TAGCLOSE_2 || attrValCount != 2) 
                     isOK = false;
               }
            }
            break;
         default:
            isOK = false;
            break;
      }

      if (!isOK) {
         __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Error parsing DTD declaration on line %d"), tokenizer.GetLineNumber());
         break;
      }

      if (curTokenType == XmlTokenizer::XML_TAGCLOSE_2 || curTokenType == XmlTokenizer::XML_TAGCLOSE_3)
         break;

      preTokenType = curTokenType;
   }

   return isOK;
}

/**
 *  Skip xml style sheet declaration
 */
PRIVATE bool __SkipStyleSheetDeclaration(XmlTokenizer &tokenizer, SAXHandler &handler)
{
   XmlTokenizer::tokentype_t preTokenType = XmlTokenizer::XML_STYLESHEET_START;
   CFString token;
   bool isOK = true;

   while(true) {
      XmlTokenizer::tokentype_t curTokenType = tokenizer.GetToken(token);

      switch (preTokenType) {
         case XmlTokenizer::XML_STYLESHEET_START:
            if (curTokenType != XmlTokenizer::XML_ATTRNAME) isOK = false;
            break;
         case XmlTokenizer::XML_ATTRNAME:
            if (curTokenType != XmlTokenizer::XML_EQU) isOK = false;
            break;
         case XmlTokenizer::XML_EQU:
            if (curTokenType != XmlTokenizer::XML_ATTRVALUE) isOK = false;
            break;
         case XmlTokenizer::XML_ATTRVALUE:
            if (curTokenType != XmlTokenizer::XML_ATTRNAME && 
                curTokenType != XmlTokenizer::XML_DECL_END) 
               isOK = false;
            break;
         default:
            isOK = false;
            break;
      }

      if (!isOK) {
         __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Error parsing stylesheet declaration on line %d"), tokenizer.GetLineNumber());
         break;
      }

      if (curTokenType == XmlTokenizer::XML_DECL_END)
         break;

      preTokenType = curTokenType;
   }

   return isOK;
}

/**
 *  Skip XML comment
 *
 *  @return true -- ending comment marker, '-->', is scanned
 *  @return false -- ending comment marker not found
 */
PRIVATE bool __SkipComment(XmlTokenizer &tokenizer, SAXHandler &handler)
{
   Stream *pStream = tokenizer.GetStream();
   int c;
   
   while(true) {
      if ((c = pStream->Read()) < 0) break;
      if (c == '\n') tokenizer.IncrementLineNumber();
      if (c != '-') continue;
      if ((c = pStream->Read()) < 0) break;
      if (c == '\n') tokenizer.IncrementLineNumber();
      if (c != '-') continue;
      if ((c = pStream->Read()) < 0) break;
      if (c == '\n') tokenizer.IncrementLineNumber();
      if (c != '>') { 
         // '--' should not appear within a comment except the pattern '-->'
         __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Sequence '--' shouldn't appear within a comment except '-->' on line %d"), tokenizer.GetLineNumber());
         break;   
      }
      return true;
   }

   return false;
} 

PRIVATE const pcwstr_t st_xmlValidDeclAttributes[] = 
           {
              _S("version"), _S("encoding"), _S("standalone"), NULL
           };

PRIVATE bool __IsValidXmlDeclarationAttribute(const CFString &attrName)
{
   for (int i = 0; st_xmlValidDeclAttributes[i]; ++i) {
      if (attrName == st_xmlValidDeclAttributes[i])
         return true;
   }

   return false;
}

/**
 *  XML declaration line
 */
PRIVATE bool __ParseXmlDeclarationLine(XmlTokenizer &tokenizer, SAXHandler &handler)
{
   XmlTokenizer::tokentype_t preTokenType = XmlTokenizer::XML_DECL_START;
   HashtableT<CFString, CFString> attrs;
   CFString token, attrName;
   bool isOK = true;

   while(true) {
      XmlTokenizer::tokentype_t curTokenType = tokenizer.GetToken(token);

      switch (preTokenType) {
         case XmlTokenizer::XML_DECL_START:
         case XmlTokenizer::XML_ATTRVALUE:
            if (curTokenType == XmlTokenizer::XML_ATTRNAME) {
               if (!__IsValidXmlDeclarationAttribute(token)) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Invalid XML declaration attribute (%s) on line %d"), 
                                                              static_cast<pcwstr_t>(token), 
                                                              tokenizer.GetLineNumber());
                  isOK = false;
                  break;
               }

               if (attrs.ContainsKey(token)) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Duplicated attribute (%s) on line %d"), 
                                                              static_cast<pcwstr_t>(token), 
                                                              tokenizer.GetLineNumber());
                  isOK = false;
                  break;
               }
               attrName = token;
            }
            else if (curTokenType == XmlTokenizer::XML_DECL_END && 
                     preTokenType == XmlTokenizer::XML_ATTRVALUE) {
               // nothing
            }
            else isOK = false;
            break;
         case XmlTokenizer::XML_ATTRNAME:
            if (curTokenType != XmlTokenizer::XML_EQU) isOK = false;
            break;
         case XmlTokenizer::XML_EQU:
            if (curTokenType == XmlTokenizer::XML_ATTRVALUE) {
               attrs.Set(new CFString(attrName), new CFString(token));

               if (attrName == _S("version") && token != _S("1.0")) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_WARN, _S("Declared XML version isn't 1.0"));
               }
               else if (attrName == _S("encoding") && token.CompareNoCase(_S("utf-8")) != 0) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Declared XML encoding isn't UTF-8"));
                  isOK = false;
               }
            }
            else isOK = false;
            break;
         default:
            isOK = false;
            break;
      }

      if (!isOK)
         break;

      if (curTokenType == XmlTokenizer::XML_DECL_END) {
         if (!attrs.ContainsKey(CFString("version"))) {
            __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("No XML version specified"));
            isOK = false;
         }
         break;
      }

      preTokenType = curTokenType;
   }

   return isOK;

}

PRIVATE bool __CollectElementContent(XmlTokenizer &tokenizer, CFString &token, SAXHandler &handler)
{
   if (!tokenizer.CollectElementContent(token)) {
      if (tokenizer.GetFetchingError() == XmlTokenizer::FERR_CDATA_END_DISALLOW)
         __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Sequence ']]>' not allowed in content on line %d"), tokenizer.GetLineNumber());
      else
         __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Error fetching string content on line %d"), tokenizer.GetLineNumber());  
      return false;
   }
   
   if (!token.IsEmpty())
      handler.ElementContent(token);
   return true;
}

PRIVATE bool __CollectCDATAContent(XmlTokenizer &tokenizer, CFString &token, SAXHandler &handler)
{
   if (!tokenizer.CollectCDATAContent(token)) {
      __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Error fetching CDATA content on line %d"), tokenizer.GetLineNumber());  
      return false;
   }
   
   if (!token.IsEmpty())
      handler.ElementContent(token);
   return true;
}

PRIVATE void __NotifyHandlerStartElement(SAXHandler &handler, const CFString &ns, const CFString &fullTagName, 
                                         collections::LinkedHashtableT<CFString, CFString> &attrs)
{
   handler.IncrementNestedLevel();
   handler.SetCurFullTagName(fullTagName);
   handler.StartElement(ns, fullTagName, attrs);
}

PRIVATE void __NotifyHandlerEndElement(SAXHandler &handler, const CFString &ns, StackCppObjT<CFString> &tagNameStack)
{
   CFString *pTagName = tagNameStack.Peek();
   handler.EndElement(ns, *pTagName);
   tagNameStack.PopAndRemove();
   
   handler.SetCurFullTagName(tagNameStack.GetCount() > 0 ? (*tagNameStack.Peek()) : CFString::Empty());
   handler.DecrementNestedLevel();
}

/**
 *  Parse XML body
 */
PRIVATE bool __ParseXmlBody(XmlTokenizer &tokenizer, SAXHandler &handler, CFString &rootNodeTag)
{
   cppmempool::MemPoolWrap memPool; // may be lots of block is allocated inside this function

   const u32_t HASHCAPACITY = 7;  // to reduce memory usage
   XmlTokenizer::tokentype_t preTokenType = XmlTokenizer::XML_TAGSTART;
   StackCppObjT<CFString> tagNameStack(false);
   LinkedHashtableT<CFString, CFString> attrs(HASHCAPACITY); 
   CFString token, attrName;
   bool isOK = true;

   tagNameStack.Push(new CFString(rootNodeTag)); // first tag on stack

   while(true) {
      XmlTokenizer::tokentype_t curTokenType = tokenizer.GetToken(token);

      switch (preTokenType) {
         case XmlTokenizer::XML_TAGSTART:
         case XmlTokenizer::XML_ATTRVALUE:
            if (curTokenType == XmlTokenizer::XML_ATTRNAME) { 
               if (attrs.ContainsKey(token)) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Duplicated attribute (%s) on line %d"), 
                                                              static_cast<pcwstr_t>(token), 
                                                              tokenizer.GetLineNumber());
                  isOK = false;
                  break;
               }
               attrName = token; 
            }
            else if (curTokenType == XmlTokenizer::XML_TAGCLOSE_1) { // '/>'
               __NotifyHandlerStartElement(handler, CFString::Empty(), *tagNameStack.Peek(), attrs);
               __NotifyHandlerEndElement(handler, CFString::Empty(), tagNameStack);
               if (tagNameStack.GetCount() > 0)
                  isOK = __CollectElementContent(tokenizer, token, handler);
            }
            else if (curTokenType == XmlTokenizer::XML_TAGCLOSE_2) { // '>'
               curTokenType = XmlTokenizer::XML_OPENINGTAG_CLOSE; // change curTokenType
               __NotifyHandlerStartElement(handler, CFString::Empty(), *tagNameStack.Peek(), attrs);
               isOK = __CollectElementContent(tokenizer, token, handler);
            }
            else isOK = false;
            break;
         case XmlTokenizer::XML_ATTRNAME:
            if (curTokenType != XmlTokenizer::XML_EQU) isOK = false;
            break;
         case XmlTokenizer::XML_EQU:
            if (curTokenType == XmlTokenizer::XML_ATTRVALUE) { 
               attrs.Set(new CFString(attrName), new CFString(token)); 
            }
            else isOK = false;
            break;
         case XmlTokenizer::XML_TAGCLOSE_1:       // '/>'
         case XmlTokenizer::XML_OPENINGTAG_CLOSE: // '>' for opening tag
         case XmlTokenizer::XML_CLOSINGTAG_CLOSE: // '>' for closing tag
            if (curTokenType == XmlTokenizer::XML_COMMENT_START) {
               if (__SkipComment(tokenizer, handler)) {
                  curTokenType = preTokenType; // revert curTokenType
                  isOK = __CollectElementContent(tokenizer, token, handler);
               }
               else isOK = false;
            }
            else if (curTokenType == XmlTokenizer::XML_TAGSTART) {
               tagNameStack.Push(new CFString(token));
               attrs.Clear();
            }
            else if (curTokenType == XmlTokenizer::XML_CLOSINGTAGSTART) {
               if (*tagNameStack.Peek() != token) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_ERROR, _S("Closing tag (%s) doesn't match opening tag (%s) on line %d"), 
                                                              static_cast<pcwstr_t>(token), static_cast<pcwstr_t>(*tagNameStack.Peek()),
                                                              tokenizer.GetLineNumber());  
                  isOK = false;
                  break;
               }
            }
            else if (curTokenType == XmlTokenizer::XML_CDATA_START) {
               if (__CollectCDATAContent(tokenizer, token, handler)) {
                  curTokenType = preTokenType; // revert curTokenType
                  isOK = __CollectElementContent(tokenizer, token, handler); // may be some data after CDATA
               }
               else isOK = false;
            }
            else if (curTokenType == XmlTokenizer::XML_STYLESHEET_START) {
               if (tokenizer.GetStyleSheetCount() > 1) {
                  __NotifyErrorToHandler(handler, SAXHandler::HERR_FATAL, _S("More than one style sheet is declared"));
                  isOK = false;
                  break;
               }

               if (!__SkipStyleSheetDeclaration(tokenizer, handler)) {
                  isOK = false;
                  break;
               }

               curTokenType = preTokenType; // revert curTokenType
            }
            else isOK = false;
            break;
         case XmlTokenizer::XML_CLOSINGTAGSTART:
            if (curTokenType == XmlTokenizer::XML_TAGCLOSE_2) {
               curTokenType = XmlTokenizer::XML_CLOSINGTAG_CLOSE; // change curTokenType
               __NotifyHandlerEndElement(handler, CFString::Empty(), tagNameStack);
               if (tagNameStack.GetCount() > 0)
                  isOK = __CollectElementContent(tokenizer, token, handler);
            }
            else isOK = false;
            break;
         
         default:
            isOK = false;
            break;
      }

      if (!isOK)
         break;

      if (tagNameStack.GetCount() == 0)
         break;

      preTokenType = curTokenType;
   } // end of while

   return isOK;
}

void XmlSaxParser::StartParsing()
{
   XmlTokenizer tokenizer(this->pStream);

   bool isXmlBodyStarted = false;          // true when any token is encountered other than comment
   bool isXmlRootElementStarted = false;   // true when the root element node is started
   CFString token;
   bool isOK = false;

   while(true) {
      XmlTokenizer::tokentype_t tokenType = tokenizer.GetToken(token);

      if (tokenType == XmlTokenizer::XML_UNKNOWN || 
          tokenType == XmlTokenizer::XML_ERROR_GENERAL) {
         __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_ERROR, _S("Error parsing token on line %d"), tokenizer.GetLineNumber()); 
         break;
      }
      else if (tokenType == XmlTokenizer::XML_STREAMEND) {
         if (!isXmlRootElementStarted)
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_ERROR, _S("No XML body is found")); 
         else
            isOK = true;
         break;
      }
      else if (tokenType == XmlTokenizer::XML_ERROR_XML_DECL_START) {
         __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_FATAL, _S("XML declaration line should have no space before it"));
         break;
      }
      else if (tokenType == XmlTokenizer::XML_DECL_START) {
         if (isXmlBodyStarted) {
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_FATAL, _S("XML declaration line must start at the first character on the first line"));
            break;
         }

         if (!__ParseXmlDeclarationLine(tokenizer, *pHandler))
            break;
      }
      else if (tokenType == XmlTokenizer::XML_COMMENT_START) {
         if (!__SkipComment(tokenizer, *pHandler))
            break;
      }
      else if (tokenType == XmlTokenizer::XML_STYLESHEET_START) {
         if (tokenizer.GetStyleSheetCount() > 1) {
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_FATAL, _S("More than one style sheet is declared"));
            break;
         }

         if (!__SkipStyleSheetDeclaration(tokenizer, *pHandler))
            break;
      }
      else if (tokenType == XmlTokenizer::XML_DOCTYPE_START) {
         if (tokenizer.GetDocTypeCount() > 1) {
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_FATAL, _S("More than one DOCTYPE is declared"));
            break;
         }

         if (isXmlRootElementStarted) {
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_FATAL, _S("DOCTYPE should be declared before XML body"));
            break;
         }

         if (!__SkipDTD(tokenizer, *pHandler))
            break;

      }
      else if (tokenType == XmlTokenizer::XML_TAGSTART) {
         if (isXmlRootElementStarted) {
            __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_ERROR, _S("More than one root element on line %d"), tokenizer.GetLineNumber()); 
            break;
         }

         isXmlRootElementStarted = true;
         pHandler->StartDocument();        
 
         if (!__ParseXmlBody(tokenizer, *pHandler, token))
            break;

         pHandler->EndDocument(); 
      }
      else {
         __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_ERROR, _S("Unexpected token (%s) on line %d"), static_cast<pcwstr_t>(token), tokenizer.GetLineNumber()); 
         break;
      }

      isXmlBodyStarted = true;    // any token can change this to true

   } // end of while

   // for any unreported error
   if (!isOK && !this->pHandler->IsErrorMarked()) {
      __NotifyErrorToHandler(*pHandler, SAXHandler::HERR_ERROR, _S("Error parsing XML on line %d"), tokenizer.GetLineNumber()); 
   }

}

} // end of namespace sax

} // end of namespace xml

} // end of namespace cppflib

